<?php
include_once __DIR__.'/XMLParserException.php';
include_once __DIR__.'/XMLNode.php';

/** 
 * A XML parser class
 * 
 * This class is used for parsing a XML document to XMLNode object class
 * 
 * @author Thuan Chau <thuanchau2020@gmail.com>
 * @package XMLParser
 */
class XMLParser {
	const TAG_NAME								= "tag";
	const TAG_TYPE								= "type";
	const TAG_LEVEL								= "level";
	const TAG_VALUE								= "value";
	const TAG_ATTRIBUTES						= "attributes";
	
	const TAG_TYPE_OPEN							= "open";
	const TAG_TYPE_CLOSE						= "close";
	const TAG_TYPE_COMPLETE						= "complete";
	const TAG_TYPE_CDATA						= "cdata";
	
	/**
	 * A XML parser resource
	 * 
	 * @access private
	 * @var resource
	 */
	private $xml_parser = NULL;

	/**
	 * The class constructor
	 * 
	 * This create a XML parser resource
	 * 
	 * @param bool $xml_option_case_folding [optional] The option of XML parser, if the value
	 * is true, the XML tag name will be in uppercase, or else the tag name in normally
	 * @access public
	 */
	public function __construct($xml_option_case_folding = false) {
		$this->xml_parser = xml_parser_create();

		xml_parser_set_option($this->xml_parser, XML_OPTION_CASE_FOLDING, ($xml_option_case_folding ? 1 : 0));
	}

	/**
	 * The class destructor
	 * 
	 * This free the XML parser resource
	 * 
	 * @access public
	 */
	public function __destruct() {
		xml_parser_free($this->xml_parser);
	}

	/**
	 * This get content of a file
	 * 
	 * @param string $xml_file_name The name of the file
	 * @return string The content of the file
	 * @access private
	 * @exception XMLParserException
	 */
	private function getContent($xml_file_name) {
		if (!($fp = fopen($xml_file_name, "r"))) {
			throw new XMLParserException(XMLParserException::MESSAGE_CAN_NOT_OPEN_FILE, XMLParserException::CODE_CAN_NOT_OPEN_FILE);
		}

		$content = fread($fp, filesize($xml_file_name));

		fclose($fp);

		return $content;
	}

	/**
	 * This parse a XML file to XMLNode object
	 * 
	 * @param string $xml_file_name The name of the file
	 * @return XMLNode A instance of XMLNode object.
	 * @access public
	 * @exception XMLParserException
	 */
	public function parse($xml_file_name) {
		// get XML file content
		$data = $this->getContent($xml_file_name);

		// parse XML file into data
		if (!xml_parse_into_struct($this->xml_parser, $data, $tags, $indexes)) {
			$xml_parser_exception = XMLParserException::tryGetException($this->xml_parser);
			if (!empty($xml_parser_exception)) {
				throw $xml_parser_exception;
			}
		}

		// parse the XML tags to XMLNode object
		$node = $this->parseToObject($tags, 0);
		
		return $node;
	}

	/**
	 * This parse the tag with index in tags array to XMLNode object
	 * 
	 * @param string[] $tags Array of tags for parsing
	 * @param int $index current parsing index of tags array
	 * @param XMLNode $current_node [optional] the current node is being parsed.
	 * @return XMLNode a XMLNode object is parsed from tags[$index]
	 * @access private
	 */
	private function parseToObject($tags, $index, $current_node = NULL) {
		// return current node if no tag to parse
		if ($index >= count($tags)) {
			return $current_node;
		}

		// get current tag
		$tag = $tags[$index];
		$tagName = $tag[self::TAG_NAME];
		$tagType = $tag[self::TAG_TYPE];
		$tagValue = isset($tag[self::TAG_VALUE]) ? $tag[self::TAG_VALUE] : "";

		switch ($tagType) {
			case self::TAG_TYPE_OPEN:
			case self::TAG_TYPE_COMPLETE:
				// create new node if tag is "open" or "complete"
				// meaning the new node is child of current node
				if (is_null($current_node)) {
					$new_node = new XMLNode(1, $tagName, $tagValue);
				} else {
					$new_node = $current_node->appendChild($tagName, $tagValue);
				}

				// if attributes is not empty, set attributes to new node
				if (!empty($tag[self::TAG_ATTRIBUTES])) {
					$new_node->setAttributes($tag[self::TAG_ATTRIBUTES]);
				}

				// if tag is "open", set current node is new node
				if ($tagType == self::TAG_TYPE_OPEN) {
					$current_node = $new_node;
				}

				break;

			case self::TAG_TYPE_CLOSE:
				// back to parent node if current node is "close"
				if (!is_null($current_node->getParentNode())) {
					$current_node = $current_node->getParentNode();
				}
				break;
		}

		// continue parse to object for next tag
		return $this->parseToObject($tags, $index + 1, $current_node);
	}
}
?>